#include <stdlib.h>
#include <stdio.h>
#include <mpi/mpi.h>
#include <string.h>
#include <sstream>
#include <iostream>
#include <fstream>
#include <set>

using namespace std;

// Declaracion de las variables globales
int ** actual;
int ** nueva;

int total_iteraciones;
int total_filas;
int total_columnas;
int Lapareamiento;
int Lsoledad;
int Lhacinamiento;
string prefijo_foto;
string nombre_estado_inicial;
int Nsnapshot;
int total_hilos;
int total_nodos;

int nodo_id;
int ** asignacion_filas;
int cantidad_filas_asignadas;
pthread_barrier_t inicio_exclusion;
pthread_barrier_t fin_exclusion;

void * codigoHilo(void *);

/*
 * Carga lee los argumentos y los copia en las variables globales.
 */
void leerArgumentos(int argc, char** argv) {

  string usage = "Usage: parallelife2 −n <N> −w <imax> −h <jmax> −la <Lapareamiento> "
          "−ls <Lsoledad> −lh <Lhacinamiento> −o <PrefijoFoto> "
          "−i <EstadoInicial> −s <NSnapshot> "
          "−m <numero de SUB−particiones por c/nodo>";

  if (argc != 21) {
    cout << usage <<endl;
    exit(EXIT_FAILURE);
  }

  for (int i = 1; i < argc; i++) {
    int j = i + 1;

    if (!strcmp(argv[i], "-n")) {
      total_iteraciones = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-w")) {
      total_columnas = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-h")) {
      total_filas = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-la")) {
      Lapareamiento = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-ls")) {
      Lsoledad = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-lh")) {
      Lhacinamiento = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-o")) {
      prefijo_foto.assign(argv[j]);

    } else if (!strcmp(argv[i], "-i")) {
      nombre_estado_inicial.assign(argv[j]);

    } else if (!strcmp(argv[i], "-s")) {
      Nsnapshot = atoi(argv[j]);

    } else if (!strcmp(argv[i], "-m")) {
      total_hilos = atoi(argv[j]);

    } else {
      cout << usage <<endl;
      exit(EXIT_FAILURE);
    }

    i = j;
  }
  if (total_nodos > total_filas) {
    cout << "Ahi mayor cantidad de nodos que filas, por lo tanto no se"
            "puede particionar" << endl;
    cout << "Intente con menos cantidad de nodos. Gracias." << endl;
    exit(EXIT_FAILURE);
  }
}

/**
 *  Guarda el la matriz en un archivo
 * @param matriz: matriz que va a ser guardada
 * @param iteracion: numero de iteracion
 */
void saveMatriz(int **matriz, int iteracion) {

    stringstream filename;
    filename << prefijo_foto << "_" << iteracion;
    string filename_str = filename.str();
    ofstream archivo(filename_str.c_str(),ios::out);

    for (int i = 0; i < total_filas; i++) {
        for (int j = 0; j < total_columnas; j++) {
            if (matriz[i][j]) {
                archivo << i <<":"<<j<<endl;
            }
        }
    }
    archivo.close();
}

/*
 * Reserva el espacio para las matrices actual y nueva.
 */
int** inicializarMatriz() {

  //Reserva de espacio para las matrices
  int** matriz = (int**) calloc(cantidad_filas_asignadas + 2, sizeof (int*));

  if (matriz == NULL) {
    cout << "Error en la reserva de memoria" << endl;
    exit(EXIT_FAILURE);
  }

  for (int i = 0; i < cantidad_filas_asignadas + 2; i++) {

    matriz[i] = (int*) calloc(total_columnas, sizeof (int));
    if (matriz[i] == NULL) {
      cout << "Error en la reserva de memoria" << endl;
      exit(EXIT_FAILURE);
    }
  }

  return matriz;
}

/*
 * Libera el espacio para las matrices actual y nueva.
 */
void liberarMatriz(int** matriz) {

  for (int i = 0; i < cantidad_filas_asignadas + 2; i++) {
    free(matriz[i]);
  }
  free(matriz);
}

/*
 *Carga el estado inicial de el archivo que se pasa como parametro en el main.
 */
void cargarEstadoInicial(int fila_asignada_inicio, int fila_asignada_fin) {
  ifstream in;
  int i, j;
  char separador;

  in.open(nombre_estado_inicial.c_str());
  while (!in.eof()) {
    in >> i >> separador >> j;
    // tomo solo las filas que estan entre mis asignadas
    if (fila_asignada_inicio <= i && i <= fila_asignada_fin && j < total_columnas) {
      // traslado la fila i que corresponde a la numeracion de las filas globales
      actual[i - fila_asignada_inicio + 1][j] = true;
    }
  }
}

/*
 * determino las filas globales que le asigno a este nodo
 */
void asignarFilasGlobales() {

  // Reservo memoria para la matriz que guarda las asignaciones de las filas
  asignacion_filas = (int**) calloc(total_nodos, sizeof (int *));
  if (asignacion_filas == NULL) {
    cout << "Error en la reserva de memoria" << endl;
    exit(EXIT_FAILURE);
  }
  for (int i = 0; i < total_nodos; i++) {
    asignacion_filas[i] = (int*) calloc(3, sizeof (int));
    if (asignacion_filas[i] == NULL) {
      cout << "Error en la reserva de memoria" << endl;
      exit(EXIT_FAILURE);
    }
  }

  int filas_restantes = total_filas;
  int nodos_restantes = total_nodos;

  //Determina que valor de fila inicial y final final le pertenece al nodo actual
  for (int i = 0; i < total_nodos; i++) {
    cantidad_filas_asignadas = filas_restantes / nodos_restantes;

    asignacion_filas[i][0] = total_filas - filas_restantes;
    asignacion_filas[i][1] = asignacion_filas[i][0] + cantidad_filas_asignadas - 1;
    asignacion_filas[i][2] = cantidad_filas_asignadas;

    filas_restantes = filas_restantes - cantidad_filas_asignadas;
    nodos_restantes--;
  }
  cantidad_filas_asignadas = asignacion_filas[nodo_id][2];
}

/*
 * Inicializo las barreras y los hilos
 */
void inicializarHilos(pthread_t * hilos) {
  pthread_attr_t attr_hilos;

  if (pthread_barrier_init(&inicio_exclusion, NULL, total_hilos + 1)) {
    printf("No hay bloques para construir el muro\n");
    exit(EXIT_FAILURE);
  }

  if (pthread_barrier_init(&fin_exclusion, NULL, total_hilos + 1)) {
    printf("No hay bloques para construir el muro\n");
    exit(EXIT_FAILURE);
  }

  if (pthread_attr_init(&attr_hilos)) {
    perror("No se pudo inicializar atributos");
    exit(EXIT_FAILURE);
  }
  pthread_attr_setscope(&attr_hilos, PTHREAD_SCOPE_PROCESS);

  int parametros_por_hilo[total_hilos][3];

  // Reparto equitativamente las filas asignadas al nodo entre los hilos del nodo
  int hilos_filas_restantes = cantidad_filas_asignadas;
  int hilos_restantes = total_hilos;
  int hilo_cant_filas_asignadas;
  for (int i = 0; i < total_hilos; i++) {
    hilo_cant_filas_asignadas = hilos_filas_restantes / hilos_restantes;

    // fila de inicio para el hilo, desplazado en una fila
    parametros_por_hilo[i][0] = cantidad_filas_asignadas - hilos_filas_restantes + 1;
    // fila de fin para el hilo, desplazado en una fila
    parametros_por_hilo[i][1] = parametros_por_hilo[i][0] + hilo_cant_filas_asignadas - 1;
    // envio el hilo_id;
    parametros_por_hilo[i][2] = i;

    hilos_filas_restantes = hilos_filas_restantes - hilo_cant_filas_asignadas;
    hilos_restantes--;
    pthread_create(&hilos[i], &attr_hilos, codigoHilo, (void *) parametros_por_hilo[i]);
  }
}

void finalizarHilos(pthread_t * hilos) {
  for (int i = 0; i < total_hilos; i++) {
    pthread_join(hilos[i], NULL);
  }
}

/*
 * Es el codigo que va a correr el nodo una vez a sido cargado los datos
 * iniciales.
 */
void sincronizarBordes() {

  // Cada nodo envia su fila superior a su vecino
  if (nodo_id == 0) {
    // el primer nodo le envia su fila superior al ultimo nodo
    MPI::COMM_WORLD.Isend(actual[1], total_columnas, MPI::INT, total_nodos - 1, 1);
  } else {
    // cualquier otro nodo, envia su fila superior al nodo superior a el
    MPI::COMM_WORLD.Isend(actual[1], total_columnas, MPI::INT, nodo_id - 1, 1);
  }

  // Cada nodo envia su fila inferior a su vecino
  if (nodo_id == total_nodos - 1) {
    // el ultimo nodo le envia su fila inferior al primer nodo
    MPI::COMM_WORLD.Isend(actual[cantidad_filas_asignadas], total_columnas, MPI::INT, 0, 1);
  } else {
    // cualquier otro nodo, envia su fila inferior al nodo inferior a el
    MPI::COMM_WORLD.Isend(actual[cantidad_filas_asignadas], total_columnas, MPI::INT, nodo_id + 1, 1);
  }

  // recibo la fila superior del vecino inferior, y la guardo abajo de mi fila inferior
  if (nodo_id == total_nodos - 1) {
    MPI::COMM_WORLD.Recv(actual[cantidad_filas_asignadas + 1], total_columnas, MPI::INT, 0, 1);
  } else {
    MPI::COMM_WORLD.Recv(actual[cantidad_filas_asignadas + 1], total_columnas, MPI::INT, nodo_id + 1, 1);
  }

  // recibo la fila inferior del vecino superior, y la guardo arriba de mi fila superior
  if (nodo_id == 0) {
    MPI::COMM_WORLD.Recv(actual[0], total_columnas, MPI::INT, total_nodos - 1, 1);
  } else {
    MPI::COMM_WORLD.Recv(actual[0], total_columnas, MPI::INT, nodo_id - 1, 1);
  }
}

int** sincronizarSnapshotNodoPrincipal() {

  //Reserva de espacio para la matriz entera
  int** matrizCompleta = (int**) calloc(total_filas, sizeof (int*));
  if (matrizCompleta == NULL) {
    cout << "Error en la reserva de memoria" << endl;
    exit(EXIT_FAILURE);
  }
  for (int i = 0; i < total_filas; i++) {
    matrizCompleta[i] = (int*) calloc(total_columnas, sizeof (int));
    if (matrizCompleta[i] == NULL) {
      cout << "Error en la reserva de memoria" << endl;
      exit(EXIT_FAILURE);
    }
  }

  // copio las filas del nodo '0' (este mismo nodo)
  for (int i = 0; i < cantidad_filas_asignadas; i++) {
    for (int j = 0; j < total_columnas; j++) {
      matrizCompleta[i][j] = actual[i + 1][j];
    }
  }

  // recibo las filas de todos los demas nodos
  int msg = true;
  for (int i = 1; i < total_nodos; i++) {
    // le indico al nodo "i" para que inicie el envio de sus filas
    MPI::COMM_WORLD.Send(&msg, 1, MPI::INT, i, 1);

    int cantidad_filas_leer = asignacion_filas[i][1] - asignacion_filas[i][0] + 1;

    for (int j = 0; j < cantidad_filas_leer; j++) {
      // desplazamiento de la fila
      int posicion_fila_guardar = asignacion_filas[i][0] + j;
      MPI::COMM_WORLD.Recv(matrizCompleta[posicion_fila_guardar], total_columnas, MPI::INT, i, 1);
    }
  }

  return matrizCompleta;
}

void sincronizarSnapshotNodoSecundario() {

  // espero que el nodo principal me indique para iniciar el envio de mis filas
  int msg;
  MPI::COMM_WORLD.Recv(&msg, 1, MPI::INT, 0, 1);

  // envio las filas al nodo principal (0)
  for (int i = 1; i < cantidad_filas_asignadas + 1; i++) {
    MPI::COMM_WORLD.Send(actual[i], total_columnas, MPI::INT, 0, 1);
  }
}

/*
 *
 */
void iterar() {
  int check_barrera;

  for (int i = 0; i < total_iteraciones; i++) {

    // espero para que los hilos terminen de hacer la iteracion de sus filas
    check_barrera = pthread_barrier_wait(&inicio_exclusion);
    if (check_barrera != 0 && check_barrera != PTHREAD_BARRIER_SERIAL_THREAD) {
      printf("Tumbe el muro.\n");
      exit(EXIT_FAILURE);
    }

    //Cambia la matriz y crea de nuevo la nueva
    liberarMatriz(actual);
    actual = nueva;
    nueva = inicializarMatriz();

    sincronizarBordes();

    check_barrera = pthread_barrier_wait(&fin_exclusion);
    if (check_barrera != 0 && check_barrera != PTHREAD_BARRIER_SERIAL_THREAD) {
      printf("Tumbe el muro.\n");
      exit(EXIT_FAILURE);
    }

    if ((i + 1) % Nsnapshot == 0) {
      if (nodo_id == 0) {
        int** matrizCompleta = sincronizarSnapshotNodoPrincipal();
        saveMatriz(matrizCompleta, i + 1);
        // Para ver las iteraciones por pantalla
        //cout << "Resultado de Iteracion: " << i + 1 << endl;
        stringstream snapshot;
        for (int iAux = 0; iAux < total_filas; iAux++) {
          for (int jAux = 0; jAux < total_columnas; jAux++) {
            snapshot << matrizCompleta[iAux][jAux] << " ";
          }
          snapshot << endl;
        }
        snapshot << endl;
        //cout << snapshot.str();
        free(matrizCompleta);
      } else {
        sincronizarSnapshotNodoSecundario();
      }
    }
  }
}

int main(int argc, char** argv) {

  //Inicializo MPI, cargo el id del nodo y la cantidad de nodos que ahi
  MPI::Init(argc, argv);
  nodo_id = MPI::COMM_WORLD.Get_rank();
  total_nodos = MPI::COMM_WORLD.Get_size();

  leerArgumentos(argc, argv);

  asignarFilasGlobales();

  // Reservo memoria para las matrices
  actual = inicializarMatriz();
  nueva = inicializarMatriz();

  // Inicializo y cargo el estado inicial
  cargarEstadoInicial(asignacion_filas[nodo_id][0], asignacion_filas[nodo_id][1]);

  // primera sincronizacion de las filas borde
  sincronizarBordes();

  // inicializo las barreras y los hilos
  pthread_t hilos[total_hilos];
  inicializarHilos(hilos);

  iterar();

  finalizarHilos(hilos);

  MPI::Finalize();

  return (EXIT_SUCCESS);
}

/*
 * Cuenta los vecinos dados una coordenada i,j.
 */
int contarVecinos(int i, int j) {
  int vecinos = 0;

  if (actual[i - 1][(j - 1 < 0 ? total_columnas - 1 : j - 1)]) { vecinos++; }
  if (actual[i - 1][j]) { vecinos++; }
  if (actual[i - 1][(j + 1 == total_columnas ? 0 : j + 1)]) { vecinos++; }

  if (actual[i][(j - 1 < 0 ? total_columnas - 1 : j - 1)]) { vecinos++; }
  if (actual[i][(j + 1 == total_columnas ? 0 : j + 1)]) { vecinos++; }

  if (actual[i + 1][(j - 1 < 0 ? total_columnas - 1 : j - 1)]) { vecinos++; }
  if (actual[i + 1][j]) { vecinos++; }
  if (actual[i + 1][(j + 1 == total_columnas ? 0 : j + 1)]) { vecinos++; }

  return vecinos;
}

/*
 * Se Verifica si en el punto i j de la matriz, la celula vive o muere
 */
void verificar_candidato(int i, int j) {
  int vecinos = contarVecinos(i,j);
//  cout << i << ":" << j << " vecinos " << vecinos << endl;

  if (actual[i][j]) {
    if (Lsoledad <= vecinos && vecinos <= Lhacinamiento) {
      nueva[i][j] = true;
    }
  } else {
    if (vecinos == Lapareamiento) {
      nueva[i][j] = true;
    }
  }
}

/**
 * buscar las celdas candidatas
 **/
set< pair<int, int > > *obtener_candidatos(int fila_ini, int fila_fin) {
  set<pair<int, int> > *retorno = new set<pair<int, int> >;

//  cout << "candidatos en fila " << endl;
  for (int i = fila_ini; i <= fila_fin; i++) {
    for (int j = 0; j < total_columnas; j++) {
      if (actual[i][j]) {
        retorno->insert(make_pair(i, (j - 1 < 0 ? total_columnas - 1 : j - 1)));
        retorno->insert(make_pair(i, j));
        retorno->insert(make_pair(i, (j + 1 == total_columnas ? 0 : j + 1)));
        if (i - 1 > 0) {
          retorno->insert(make_pair(i - 1, (j - 1 < 0 ? total_columnas - 1 : j - 1)));
          retorno->insert(make_pair(i - 1, j));
          retorno->insert(make_pair(i - 1, (j + 1 == total_columnas ? 0 : j + 1)));
        }

        if (i + 1 < cantidad_filas_asignadas + 1) {
          retorno->insert(make_pair(i + 1, (j - 1 < 0 ? total_columnas - 1 : j - 1)));
          retorno->insert(make_pair(i + 1, j));
          retorno->insert(make_pair(i + 1, (j + 1 == total_columnas ? 0 : j + 1)));
        }
      }
    }

    if (fila_ini == 1) {
      for (int j = 0; j < total_columnas; j++) {
        if (actual[0][j]) {
          retorno->insert(make_pair(1, (j - 1 < 0 ? total_columnas - 1 : j - 1)));
          retorno->insert(make_pair(1, j));
          retorno->insert(make_pair(1, (j + 1 == total_columnas ? 0 : j + 1)));
        }
      }
    }
    if (fila_fin == cantidad_filas_asignadas) {
      for (int j = 0; j < total_columnas; j++) {
        if (actual[cantidad_filas_asignadas + 1][j]) {
          retorno->insert(make_pair(cantidad_filas_asignadas, (j - 1 < 0 ? total_columnas - 1 : j - 1)));
          retorno->insert(make_pair(cantidad_filas_asignadas, j));
          retorno->insert(make_pair(cantidad_filas_asignadas, (j + 1 == total_columnas ? 0 : j + 1)));
        }
      }
    }
  }
  return retorno;
}

void *codigoHilo(void *parametros_hilo) {
  int check_barrera;

  int fila_ini = ((int *) parametros_hilo)[0];
  int fila_fin = ((int *) parametros_hilo)[1];
  //int hilo_id = ((int *) parametros_hilo)[2];

  set< pair<int, int> > *candidatos;

  for (int i = 0; i < total_iteraciones; i++) {

    candidatos = obtener_candidatos(fila_ini, fila_fin);

    set<pair<int, int> >::iterator set_iterator;
    for (set_iterator = candidatos->begin(); set_iterator != candidatos->end(); set_iterator++) {
      verificar_candidato(set_iterator->first, set_iterator->second);
    }

    check_barrera = pthread_barrier_wait(&inicio_exclusion);
    if (check_barrera != 0 && check_barrera != PTHREAD_BARRIER_SERIAL_THREAD) {
      printf("Tumbe el muro.\n");
      exit(EXIT_FAILURE);
    }

    check_barrera = pthread_barrier_wait(&fin_exclusion);
    if (check_barrera != 0 && check_barrera != PTHREAD_BARRIER_SERIAL_THREAD) {
      printf("Tumbe el muro.\n");
      exit(EXIT_FAILURE);
    }

    free(candidatos);
  }

  pthread_exit(EXIT_SUCCESS);

}